// License Notification
//======================================================
// Copyright (c) 2010 Channel Intelligence Inc.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// ======================================================




using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Text;
using CIP4.Common;
using CIP4.Item;
using CIP4.Simple;

namespace CIP4.DataProvider.S3
{
    internal delegate bool SetHeaderPropertiesDelegate(object item, CollectionType collection, ISerializerType serializer);

    public class DataProvider : CIP4.Simple.DataProvider, IDataProvider
    {
        string AccessKey { get; set; }

        // Given a BACKOFFDELTA of 10 and MAXRETRIES of 8 will wait for 
        // 10, 20, 40, 80, 160, 320, 640, 1280, 2560 milliseconds

        private const int MAXRETRIES = 8;

        private const int BACKOFFDELTA = 10;

        #region IDataProvider Members

        public string ConnectionString { get; private set; }

        private IDataProvider Cache { get; set; }

        public DataProvider (CIP4ConnectionAttribute DataProviderConnection, IDataProvider cachingProvider, LoggerDelegate logger)
            : base (DataProviderConnection)
        {
            if (!(DataProviderConnection is CIP4S3ConnectionAttribute))
                LoggerWrapper(MajorEnum.Error, MinorEnum.UnexpectedConnectionType,
                    "{0} is not an acceptable connection type", new object[] { DataProviderConnection.ToString() }, logger);
            else
            {
                LoggerWrapper(MajorEnum.Trace, MinorEnum.InitializingProvider,
                    "Initalizing Provider: {0}", new object[] { DataProviderConnection }, logger);
                CIP4S3ConnectionAttribute connection = (CIP4S3ConnectionAttribute)DataProviderConnection;
                AccessKey = connection.AccessKey;
                S3BucketType.Initialize(AccessKey, connection.SecretKey);
                ConnectionString = connection.ToString();
                Cache = cachingProvider;
            }
        }

        public void DeleteCollection(CollectionType collection, LoggerDelegate logger)
        {
            if (!LoggerWrapper(MajorEnum.Trace, MinorEnum.ActionStart,
                "Started {0}", new object[] { MinorEnum.DeleteCollection }, logger))
                return;

            S3BucketType bucket = S3BucketType.GetBucket(collection, null, LoggerWrapper);
            if (Cache != null)
                Cache.DeleteCollection(collection, logger);
            if (bucket.DeleteCollection(collection, logger))
                LoggerWrapper(MajorEnum.Information, MinorEnum.DeleteCollection,
                    "Collection deleted {0}", new object[] { collection }, logger);
        }

        public override IEnumerable<object> GetItems(CollectionType collection, IBooleanValue selectionExpression, CursorType cursor)
        {
            int retryCount = 0;
            string saveMarker = cursor.Marker;

            List<object> items = new List<object>();
            if (!LoggerWrapper(MajorEnum.Trace, MinorEnum.ActionStart,
                "Started {0}", new object[] { MinorEnum.GetItems }, cursor.Logger))
                return items;

            try
            {
                while (true)
                {
                    try
                    {
                        items = InnerGetItems(collection, selectionExpression, cursor);
                        return items;
                    }
                    catch (System.Net.WebException ex)
                    {
                        if ((retryCount += 1) >= MAXRETRIES)
                        {
                            LoggerWrapper(MajorEnum.Error, MinorEnum.S3GetItems, "GetItems retry count exceeded: {0}", new object[] { ex }, cursor.Logger);
                            break;
                        }
                        LoggerWrapper(MajorEnum.Error, MinorEnum.S3GetItems, "WebException getting items from {0}, retryCount={1}, exception={2}", new object[] { collection.Name, retryCount, ex.Message }, cursor.Logger);
                        System.Threading.Thread.Sleep(BACKOFFDELTA * 2 ^ retryCount);
                        cursor.Marker = saveMarker;
                    }
                    catch (System.Web.Services.Protocols.SoapException ex)
                    {
                        if ((retryCount += 1) >= MAXRETRIES)
                        {
                            LoggerWrapper(MajorEnum.Error, MinorEnum.SoapExceptionOnGet, "GetItems retry count exceeded: {0}", new object[] { ex }, cursor.Logger);
                            break;
                        }
                        LoggerWrapper(MajorEnum.Error, MinorEnum.SoapExceptionOnGet, "SoapException getting items from {0}, retryCount={1}, exception={2}", new object[] { collection.Name, retryCount, ex.Message }, cursor.Logger);
                        System.Threading.Thread.Sleep(BACKOFFDELTA * 2 ^ retryCount);
                        cursor.Marker = saveMarker;
                    }
                }
            }
            finally
            {
                LoggerWrapper(MajorEnum.Information, MinorEnum.GetItems,
                    "Get {0} items from {1}", new object[] { items.Count, collection }, cursor.Logger);
            }

            return new List<object>();
        }

        public override void PurgeItems(IEnumerable<ExternalIDType> externalIDs, CollectionType collection, GetSerializerDelegate getSerializer)
        {
            ISerializerType serializer = getSerializer(SerializerKindEnum.CIP4Xml);

            if (!LoggerWrapper(MajorEnum.Trace, MinorEnum.ActionStart,
                "Started {0}", new object[] { MinorEnum.PurgeItems }, serializer.Logger))
                return;

            int retryCount = 0;

            while (true)
            {
                try
                {
                    int deletedCount = InnerPurgeItems(externalIDs, collection, getSerializer);
                    LoggerWrapper(MajorEnum.Information, MinorEnum.PurgeItems,
                        "{0} items deleted from {1}", new object[] { deletedCount, collection.Name }, serializer.Logger);
                    break;
                }
                catch (System.Net.WebException ex)
                {
                    if ((retryCount += 1) >= MAXRETRIES)
                    {
                        LoggerWrapper(MajorEnum.Error, MinorEnum.S3PurgeItems, "GetItems retry count exceeded: {0}", new object[] { ex }, serializer.Logger);
                        break;
                    }
                    LoggerWrapper(MajorEnum.Error, MinorEnum.S3PurgeItems, "WebException getting items from {0}, retryCount={1}, exception={2}", new object[] { collection.Name, retryCount, ex.Message }, serializer.Logger);
                    System.Threading.Thread.Sleep(BACKOFFDELTA * 2 ^ retryCount);
                }
                catch (System.Web.Services.Protocols.SoapException ex)
                {
                    if ((retryCount += 1) >= MAXRETRIES)
                    {
                        LoggerWrapper(MajorEnum.Error, MinorEnum.S3SoapExceptionOnPurge, "GetItems retry count exceeded: {0}", new object[] { ex }, serializer.Logger);
                        break;
                    }
                    LoggerWrapper(MajorEnum.Error, MinorEnum.S3SoapExceptionOnPurge, "SoapException getting items from {0}, retryCount={1}, exception={2}", new object[] { collection.Name, retryCount, ex.Message }, serializer.Logger);
                    System.Threading.Thread.Sleep(BACKOFFDELTA * 2 ^ retryCount);
                }
            }
        }

        public void SaveItems(IEnumerable<object> items, CollectionType collection, GetSerializerDelegate getSerializer)
        {
            ISerializerType serializer = getSerializer(SerializerKindEnum.CIP4Xml);

            if (!LoggerWrapper(MajorEnum.Trace, MinorEnum.ActionStart,
                "Started {0}", new object[] { MinorEnum.SaveItems }, serializer.Logger))
                return;

            int retryCount = 0;
            
            while (true)
            {
                try
                {
                    int itemCount = InnerSaveItems(items, collection, getSerializer);
                    LoggerWrapper(MajorEnum.Information, MinorEnum.SaveItems,
                        "{0} items saved in {1}", new object[] { itemCount, collection.Name }, serializer.Logger);
                    break;
                }
                catch (System.Net.WebException ex)
                {
                    if ((retryCount += 1) >= MAXRETRIES)
                    {
                        LoggerWrapper(MajorEnum.Error, MinorEnum.S3SaveItems, "GetItems retry count exceeded: {0}", new object[] { ex }, serializer.Logger);
                        break;
                    }
                    LoggerWrapper(MajorEnum.Warning, MinorEnum.S3SaveItems, "WebException getting items from {0}, retryCount={1}, exception={2}", new object[] { collection.Name, retryCount, ex.Message }, serializer.Logger);
                    System.Threading.Thread.Sleep(BACKOFFDELTA * 2 ^ retryCount);
                }
                catch (System.Web.Services.Protocols.SoapException ex)
                {
                    if ((retryCount += 1) >= MAXRETRIES)
                    {
                        LoggerWrapper(MajorEnum.Error, MinorEnum.S3SoapExceptionOnSave, "GetItems retry count exceeded: {0}", new object[] { ex }, serializer.Logger);
                        break;
                    }
                    LoggerWrapper(MajorEnum.Error, MinorEnum.S3SoapExceptionOnSave, "SoapException getting items from {0}, retryCount={1}, exception={2}", new object[] { collection.Name, retryCount, ex.Message }, serializer.Logger);
                    System.Threading.Thread.Sleep(BACKOFFDELTA * 2 ^ retryCount);
                }
            }
        }

        public void Terminate(LoggerDelegate logger)
        {
        }

        #endregion

        #region Helper Functions

        private List<object> InnerGetItems(CollectionType collection, IBooleanValue selectionExpression, CursorType cursor)
        {
            ISerializerType serializer = cursor.GetSerializer(SerializerKindEnum.CIP4Xml);

            string lookForExternalId = GetExternalIdValueFromSelectionExpression(selectionExpression, serializer);

            if (Cache != null && !string.IsNullOrEmpty(lookForExternalId))
            {
                // if we can find it in the cache, no point in looking in the s3, as an id has 
                // been specified on the given template item. 
                List<object> result = new List<object>(Cache.GetItems(collection, selectionExpression, cursor));
                if (result.Count > 0)
                {
                    LoggerWrapper(MajorEnum.Trace, MinorEnum.CacheHit, "Found {0} in the cache", new object[] { lookForExternalId }, cursor.Logger);
                    return result;
                }
            }

            S3BucketType bucket = S3BucketType.GetBucket(collection, null, LoggerWrapper);

            if (!string.IsNullOrEmpty(lookForExternalId))
            {
                List<object> results = bucket.GetItems(selectionExpression, cursor);
                if (Cache != null)
                    Cache.SaveItems(results, collection, cursor.GetSerializer);
                return results;
            }
            else
            {
                if (cursor.FailOnLinearSearch)
                {
                    if (!LoggerWrapper(MajorEnum.Error, MinorEnum.FailOnLinearSearch,
                        "Failed on linear search accessing {0}", new object[] { collection }, cursor.Logger))
                        return new List<object>();
                }
                List<object> results = bucket.GetItems(selectionExpression, cursor);

                if (Cache != null)
                    Cache.SaveItems(results, collection, cursor.GetSerializer);

                return results;
            }
        }

        private int InnerPurgeItems(IEnumerable<ExternalIDType> externalIDs, CollectionType collection, GetSerializerDelegate getSerializer)
        {
            S3BucketType bucket = S3BucketType.GetBucket(collection, null, LoggerWrapper);
            if (Cache != null)
                Cache.PurgeItems(externalIDs, collection, getSerializer);

            return bucket.PurgeItems(externalIDs, getSerializer(SerializerKindEnum.CIP4Xml));
        }

        private int InnerSaveItems(IEnumerable<object> items, CollectionType collection, GetSerializerDelegate getSerializer)
        {
            S3BucketType bucket = S3BucketType.GetBucket(collection, null, LoggerWrapper);
            if (Cache != null)
                Cache.SaveItems(items, collection, getSerializer);

            return bucket.SaveItems(items, collection, getSerializer(SerializerKindEnum.CIP4Xml), SetHeaderProperties);
        }

        #endregion
    }
}
